Skip to main content 집밥서선생

Timer와 Ticker

Published: 2023-09-14

Timer

Go에서 일정 시간이 지난 이후 특정 작업을 수행하려면 time.Timer를 사용하면 된다. time.Timer는 지정된 시간만큼 대기한 후 채널에 값을 전송한다. 이 때 채널에 전송되는 값은 메시지가 전송된 시간이며, 수신된 시간이 아님을 염두에 두어야 한다.

타이머는 주로 작업의 타임아웃을 설정할 때 사용한다. 타임아웃 자체는 context.Context를 사용하여 구현하는 것이 좋지만, 타이머를 사용하는 방법도 알아두면 좋다.

func main() {
	timer := time.NewTimer(10 * time.Millisecond)
	timeout := make(chan struct{})

	go func() {
		<-timer.C
		close(timeout)
		fmt.Println("Timer expired")
	}()

	x := 0
	done := false
	for !done {
		select {
		case <-timeout:
			done = true
		default:
		}

		time.Sleep(1 * time.Millisecond)
		x++
	}
	fmt.Println("x =", x)
}
Show⯆

time.NewTimer 대신 time.After를 사용할 수도 있다. 사용법은 거의 동일하므로 예제는 생략해도 될 듯 하다. 또한 이와 같은 예제는 time.AfterFunc을 사용하여 더 간단하게 작성할 수 있다.

func main() {
timeout := make(chan struct{})

    time.AfterFunc(10*time.Millisecond, func() {
    	close(timeout)
    	fmt.Println("Timer expired")
    })

}

어떤 작업이 타임아웃을 초과하기 이전에 정상적으로 완료되었다면 타이머를 중지해야 한다. 타이머를 중지하려면 Stop 메서드를 호출하면 된다. 타이머가 정상적으로 중지되었다면 Stop 메서드는 true를 반환하고, 이미 중지되었다면 false를 반환한다.

타이머를 재설정하려면 Reset 메서드를 호출하면 된다.

  • AfterFunc로 생성된 타이머의 경우 아직 호출되지 않았다면 호출될 시간을 재설정하며, 이미 호출되었다면 한 번 더 호출될 시간이 설정된다. 전자의 경우 true가, 후자의 경우 false가 반환된다.

  • NewTimer로 생성된 타이머의 경우, Reset 메서드를 사용할 때 동시에 해당 타이머 채널으로부터 값을 수신하고 있는 고루틴이 있어선 안된다. 아래 예제가 Reset 메서드를 사용할 때의 올바른 사용법이다.

    select {
    	case <-timer.C:
    		// timeout
    	case d := <-resetTimer:
    		if !timer.Stop() {
    			<-timer.C
    		}
    		timer.Reset(d)
    }


Tickers


time.Ticker는 지정된 시간 간격으로 특정 작업을 수행하려고 할 때 사용한다. time.Ticker 또한 마찬가지로 time.NewTicker를 사용하여 생성하며, 명시적으로 중지하기 전까지 주기적으로 채널에 값을 전송한다. 다음의 예제는 3초간 주기적으로 프로그램의 경과 시간을 출력하는 예제이다.

func main() {
	start := time.Now()
	ticker := time.NewTicker(100 * time.Millisecond)
	defer ticker.Stop()
	done := time.After(3 * time.Second)

	for {
		select {
		case <-ticker.C:
			fmt.Println("tick:", time.Since(start).Milliseconds())
		case <-done:
			return
		}
	}
}
Show⯆

Ticker의 간격보다 작업 시간이 길어져서 Ticker가 보내는 신호를 놓치는 경우가 생길 수 있다. 만약 Ticker 채널에서 다음 값이 발생하기 전에 값을 읽었다면 단순이 약간 늦게 읽었을 뿐이라 큰 차이가 없다. 반면 Ticker 채널에서 값을 읽지 못해 다음 값이 이미 발생하는 경우가 있을 수 있다. 이 경우 Ticker 채널에서 값을 읽으면 쌓여있는 값을 와다닥 다 읽는 게 아니라, 놓친 값들은 모두 버려지고 최신 값 하나만 읽게 된다.

Ticker를 모두 사용했다면 반드시 Stop() 메서드를 호출하여 중지해야 한다. Timer와 달리 Ticker는 자동으로 중지되지 않기 때문에 가비지 컬렉터가 수거해가지 못한다. defer ticker.Stop()를 통해 Ticker를 중지하는 것을 잊지 않도록 하자.

Heartbeats

Long-running 작업을 수행하는 경우, 작업이 정상적으로 수행되고 있는지 주기적으로 모니터링해야 할 필요가 있다. 이 경우 Long-running 함수가 모니터 함수에 Heartbeat를 보내어 작업이 정상적으로 수행되고 있는지 알려줄 수 있다. 모니터 함수는 일정 시간동안 Heartbeat를 받지 못하면 Long-running 함수가 정상적으로 수행되지 않고 있다고 판단할 수 있다.

func monitor(heartbeat <-chan struct{}, done chan struct{}, tick <-chan time.Time) {
	var lastHeartbeat time.Time
	var numTicks int

	for {
		select {
		case <-tick:
			numTicks++
			if numTicks >= 2 {
				fmt.Printf("No progress since %s, exiting\n", lastHeartbeat)
				close(done)
				return
			}

		case <-heartbeat:
			lastHeartbeat = time.Now()
			numTicks = 0
		}
	}
}

func longRunningFunction(heartbeat chan<- struct{}, done chan struct{}) {
	for i := 0; i < 10; i++ {
		select {
		case <-done:
			return
		case heartbeat <- struct{}{}:
		}
		fmt.Printf("Job %d\n", i)
		time.Sleep(500 * time.Millisecond)
	}
	close(done)
}

func main() {
	heartbeat := make(chan struct{})
	defer close(heartbeat)
	done := make(chan struct{})
	tick := time.NewTicker(1 * time.Second)
	defer tick.Stop()

	go monitor(heartbeat, done, tick.C)
	go longRunningFunction(heartbeat, done)

	<-done
	fmt.Println("Long running function finished")
}
Show⯆

위 코드에서 monitor 함수는 2회 연속으로 Tick에서 신호를 받는 동안 longRunningFunction으로부터 Heartbeat를 받지 못하면 문제가 생긴 것으로 판단하고 longRunningFunction을 종료한다. 이 예제에서 heartbeat 채널은 단순히 struct{} 타입이지만 다양한 메타데이터를 포함할 수 있다.



References


[

Effective Concurrency in Go ](https://learning.oreilly.com/library/view/effective-concurrency-in/9781804619070/)
[Burak Serdar, 『Effective Concurrency in Go』, Packt Publishing](https://learning.oreilly.com/library/view/effective-concurrency-in/9781804619070/)

© 2024 JHSeo. All right reserved.